﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Oxage.Common
{
	public partial class PluginManager<T>
	{
		#region Private members
		private string pluginBasePath = null;
		private List<string> loadedFiles = null;
		private FileSystemWatcher watcher = null;
		#endregion

		#region Constructor
		/// <summary>
		/// Initializes a new PluginManager instance.
		/// </summary>
		public PluginManager()
		{
			//Default values
			this.loadedFiles = new List<string>();
			this.Plugins = new List<Plugin<T>>();
			this.PluginBasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
			this.AutoLoad = false;
		}
		#endregion

		#region Public properties
		/// <summary>
		/// Gets or sets a custom logger.
		/// </summary>
		/// <summary>
		/// Gets a list of loaded plugins.
		/// </summary>
		public List<Plugin<T>> Plugins
		{
			get;
			protected set;
		}

		/// <summary>
		/// Gets or sets a plugin directory.
		/// </summary>
		public string PluginBasePath
		{
			get
			{
				return this.pluginBasePath;
			}
			set
			{
				this.pluginBasePath = value;
				if (this.watcher != null)
				{
					this.watcher.Path = this.pluginBasePath;
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether automatic plugin loading is enabled.
		/// Enabled means that when a .dll file is copied to a plugin directory it is automatially loaded into the application domain.
		/// </summary>
		public bool AutoLoad
		{
			get
			{
				return (this.watcher != null);
			}
			set
			{
				if (value)
				{
					loadFileSystemWatcher();
				}
				else
				{
					unloadFileSystemWatcher();
				}
			}
		}
		#endregion

		#region Private methods
		/// <summary>
		/// Initializes a new FileSystemWatcher instance.
		/// </summary>
		private void loadFileSystemWatcher()
		{
			unloadFileSystemWatcher();
			this.watcher = new FileSystemWatcher(this.pluginBasePath);
			this.watcher.IncludeSubdirectories = false;
			this.watcher.Filter = "*.dll";
			this.watcher.Created += new FileSystemEventHandler(PluginFileCreated);
			this.watcher.EnableRaisingEvents = true;
		}

		/// <summary>
		/// Unloads existing FileSystemWatcher.
		/// </summary>
		private void unloadFileSystemWatcher()
		{
			if (this.watcher != null)
			{
				this.watcher.EnableRaisingEvents = false;
				this.watcher.Created -= new FileSystemEventHandler(PluginFileCreated);
				this.watcher = null;
			}
		}
		#endregion

		#region Public methods
		/// <summary>
		/// Loads plugins from .dll assemblies in the plugin directory.
		/// </summary>
		public void LoadPlugins()
		{
			if (Directory.Exists(this.PluginBasePath))
			{
				string[] files = Directory.GetFiles(this.PluginBasePath, "*.dll", SearchOption.TopDirectoryOnly);
				foreach (string file in files)
				{
					//Avoid loading plugin twice
					if (!this.loadedFiles.Contains(file))
					{
						try
						{
							List<Plugin<T>> plugins = LoadAssembly(file);
							if (plugins != null && plugins.Count > 0)
							{
								this.Plugins.AddRange(plugins);
								this.loadedFiles.Add(file);
								//Logger.WriteLine("Plugins from " + Path.GetFileName(file) + " have been loaded!");
							}
						}
						catch (Exception ex)
						{
							//Logger.Log(ex);
						}
					}
				}
			}
		}
		#endregion

		#region Protected methods
		/// <summary>
		/// Loads plugins from an assembly file.
		/// </summary>
		/// <param name="path">Full path to an assembly file.</param>
		/// <returns>Returns list of loaded plugins.</returns>
		protected List<Plugin<T>> LoadAssembly(string path)
		{
			List<Plugin<T>> result = new List<Plugin<T>>();
			Assembly asm = Assembly.LoadFile(path);
			Type[] types = asm.GetExportedTypes();

			foreach (Type type in types)
			{
				PluginAttribute a = null;

				#region Get PluginAttribute
				object[] attributes = type.GetCustomAttributes(typeof(PluginAttribute), false);
				if (attributes != null && attributes.Length > 0)
				{
					foreach (object attribute in attributes)
					{
						if (attribute is PluginAttribute)
						{
							a = attribute as PluginAttribute;
							break;
						}
					}
				}
				#endregion

				if (a != null && typeof(T).IsAssignableFrom(type))
				{
					Plugin<T> plugin = new Plugin<T>();
					plugin.Item = (T)Activator.CreateInstance(type);
					plugin.Name = a.Name;
					plugin.Description = a.Description;
					plugin.Filename = path;
					result.Add(plugin);
				}
			}

			return result;
		}
		#endregion

		#region Event handlers
		/// <summary>
		/// Event is triggered when an assembly file is copied to the plugin directory.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected virtual void PluginFileCreated(object sender, FileSystemEventArgs e)
		{
			List<Plugin<T>> plugins = LoadAssembly(e.FullPath);
			this.Plugins.AddRange(plugins);
		}
		#endregion
	}
}
